// Copyright 2013 The Go Circuit Project
// Use of this source code is governed by the license for
// The Go Circuit Project, found in the LICENSE file.
//
// Authors:
//   2013 Petar Maymounkov <p@gocircuit.org>
//   2014 Chris Monson <chris@gocircuit.org>

package ripple

import (
	"fmt"
	"log"
	"math/rand"
	"path"
	"sync"
	"time"

	"circuit/kit/rh"
	"circuit/kit/rh/ns"
)

const (
	MUTEX_DIR_NAME  = "Mutex"
	PROC_DIR_NAME   = "Proc"
	CHAN_DIR_NAME   = "Chan"
	SELECT_DIR_NAME = "Select"
)

func buildSlabHelp(name string, id uint64) string {
	return fmt.Sprintf(slabHelpFmt, path.Join("/", name), id)
}

const slabHelpFmt = `
  This is the directory for a slab ripple, named "%s", with QID ⟨Q%016x⟩.

  SYNOPSIS

  cat help   # Read this help screen.

  cd Proc    # Access the proc ripple (create and manage processes)
  cd Mutex   # Access the mutex ripple (create and manage mutexes)
  cd Chan    # Access the chan ripple (create and manage channels)
  cd Select  # Access the select ripple (create and manage select statements)

  Making a new child subdirectory will create a subordinate slab ripple.
  Slab directories are garbage-collected.

`

// Slab
type Slab struct {
	// immutable
	cmd   map[string]rh.FID
	name  string
	gtm   time.Time
	q     rh.Q
	clunk func()
	// mutable
	z struct {
		sync.Mutex
		atm    time.Time
		ripple map[string]RippleFID
	}

	mutex   *EndPointDir
	proc    *EndPointDir
	channel *EndPointDir
	sel     *EndPointDir

	mutexFID   rh.FID
	procFID    rh.FID
	channelFID rh.FID
	selectFID  rh.FID

	helpFID        rh.FID
	mutexHelpFID   rh.FID
	procHelpFID    rh.FID
	channelHelpFID rh.FID
	selectHelpFID  rh.FID
}

type RippleFID interface {
	rh.FID
}

// NewSlab
func NewSlab(name string) (*Slab, rh.FID) {
	now := time.Now()
	s := &Slab{
		name: name,
		gtm:  now,
		q: rh.Q{
			ID:  uint64(rand.Int63()),
			Ver: 1,
		},
	}
	s.z.atm, s.z.ripple = now, make(map[string]RippleFID)

	s.mutex, s.mutexFID = NewEndPointDir(
		"mutex",
		MUTEX_DIR_NAME,
		func(name string) rh.FID {
			_, fid := NewMutex(name)
			return fid
		})

	s.proc, s.procFID = NewEndPointDir(
		"proc",
		PROC_DIR_NAME,
		func(name string) rh.FID {
			_, fid := NewProc(name)
			return fid
		})

	s.channel, s.channelFID = NewEndPointDir(
		"chan",
		CHAN_DIR_NAME,
		func(name string) rh.FID {
			_, fid := NewChan(name)
			return fid
		})

	s.sel, s.selectFID = NewEndPointDir(
		"select",
		SELECT_DIR_NAME,
		func(name string) rh.FID {
			_, fid := NewSelect(name)
			return fid
		})

	s.helpFID = NewHelp(buildSlabHelp(name, s.q.ID))
	s.mutexHelpFID = NewHelp(s.mutex.Help())
	s.procHelpFID = NewHelp(s.proc.Help())
	s.channelHelpFID = NewHelp(s.channel.Help())
	s.selectHelpFID = NewHelp(s.sel.Help())

	s.cmd = map[string]rh.FID{
		MUTEX_DIR_NAME:  s.mutexFID,
		PROC_DIR_NAME:   s.procFID,
		CHAN_DIR_NAME:   s.channelFID,
		SELECT_DIR_NAME: s.selectFID,
		"help":          s.helpFID}

	return s, s.Walk()
}

func (s *Slab) MakeSlab(name string) (*Slab, rh.FID, error) {
	s.z.Lock()
	defer s.z.Unlock()
	//
	if !s.approve(name) {
		log.Printf("Error: slab name %q not approved", name)
		return nil, nil, rh.ErrExist
	}
	t, fid := NewSlab(name)
	s.add(name, fid)
	return t, fid, nil
}

func (s *Slab) approve(name string) bool {
	_, ok := s.z.ripple[name]
	if ok {
		return false
	}
	_, ok = s.cmd[name]
	if ok {
		return false
	}
	return true
}

func (s *Slab) add(name string, r RippleFID) {
	if !s.approve(name) {
		panic("u")
	}
	s.z.ripple[name] = ns.NewFID(name, r)
}
func (s *Slab) toc() (toc rh.DirChunk) {
	defer un(lock(&s.z))
	// List attachment files
	for _, cmdFID := range s.cmd {
		dir, err := cmdFID.Stat()
		if err != nil {
			panic(err)
		}
		toc = append(toc, dir)
	}
	// List sub-slabs
	for _, rFID := range s.z.ripple {
		dir, err := rFID.Stat()
		if err != nil {
			panic(err)
		}
		toc = append(toc, dir)
	}
	return
}

func (s *Slab) Dump(fix string) {
	defer un(lock(&s.z))
	for name, r := range s.z.ripple {
		fmt.Printf("%s %s\n", fix, name)
		slb, ok := r.(*slabFID)
		if !ok {
			continue
		}
		slb.s.Dump(fix + " " + name)
	}
}

func (s *Slab) ClunkWith(clunk func()) {
	s.clunk = clunk
}

func (s *Slab) scrub(name string, r RippleFID) {
	defer un(lock(&s.z))

	x, ok := s.z.ripple[name]
	if !ok {
		return
	}
	if r != x {
		return
	}
	delete(s.z.ripple, name)
}

func (s *Slab) Walk() rh.FID {
	return &slabFID{s: s}
}

// slabFID is the RH-facing side of a slab
type slabFID struct {
	rh.AnchoredFID
	rh.DontWriteFID
	//
	s *Slab
}

func (fid *slabFID) Clunk() {
	// Don't do anything.
	// When all pointers to this FID are gone, it will scrub itself from its parent automatically.
}

//
func (fid *slabFID) String() string {
	return "slab"
}

//
func (fid *slabFID) Q() rh.Q {
	return fid.s.q
}

// Open is nop.
func (fid *slabFID) Open(rh.Flag, rh.Intr) error {
	return nil
}

// Create allows creation of subdirectories, which would themselves be slabs
func (fid *slabFID) Create(name string, _ rh.Flag, mode rh.Mode, _ rh.Perm) (rh.FID, error) {
	defer func() {
		fid.s.Dump("")
	}()
	if mode.Attr != rh.ModeDir {
		return nil, rh.ErrPerm
	}
	_, fid2, err := fid.s.MakeSlab(name)
	if err != nil {
		return nil, err
	}
	return fid2, nil
}

func (fid *slabFID) Stat() (*rh.Dir, error) {
	defer un(lock(&fid.s.z))

	now := time.Now()
	return &rh.Dir{
		Q:      fid.s.q,
		Mode:   rh.Mode{Attr: rh.ModeDir},
		Perm:   0777,
		Atime:  now, //fid.s.z.atm,
		Mtime:  now, //fid.s.z.atm,
		Name:   fid.s.name,
		Length: int64(len(fid.s.cmd) + len(fid.s.z.ripple)),
		Uid:    uid(),
		Gid:    gid(),
	}, nil
}

//
func (fid *slabFID) Read(offset int64, count int, _ rh.Intr) (rh.Chunk, error) {
	var toc rh.DirChunk
	toc = fid.s.toc()
	if count == 0 {
		count = len(toc)
	}
	if int(offset) >= len(toc) {
		return nil, nil
	}
	toc = toc[offset:min(len(toc), int(offset)+count)]
	return toc, nil
}

func (fid *slabFID) Walk(wname []string) (rh.FID, error) {
	if len(wname) == 0 {
		return fid.s.Walk(), nil
	}
	if len(wname) > 1 {
		return nil, rh.ErrClash
	}
	// command?
	if cmdFID, ok := fid.s.cmd[wname[0]]; ok {
		return cmdFID.Walk(nil)
	}
	// ripple?
	if rFID, ok := fid.s.z.ripple[wname[0]]; ok {
		return rFID.Walk(nil)
	}
	return nil, rh.ErrNotExist
}
